Máster en Data Science UAH

Tasador de viviendas de alquiler vacacional en París

Notebook #1 - Análisis exploratorio

Alumno: Héctor Mateos Oblanca
Tutor: Daniel Rodríguez Pérez

Contenidos

Limpieza y preparación de datos

In [1]:
import math
import json
import pandas as pd
import pandas_profiling
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import chart_studio.plotly as py
import plotly.graph_objs as go
import plotly.figure_factory as ff
from plotly.colors import n_colors
from plotly.offline import iplot, init_notebook_mode
from plotly.subplots import make_subplots

init_notebook_mode(connected=True)

%run src/utils.py

Carga de datos

Dataset principal Airbnb

In [2]:
city = 'paris'
month = '201909'
filename_in = 'src/data/' + city + '-' + month + '-listings.csv'
filename_out = 'src/data/' + city + '-' + month + '-listings-CLEAN.csv'

df = pd.read_csv(filename_in, low_memory=False)
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 64970 entries, 0 to 64969
Columns: 106 entries, id to reviews_per_month
dtypes: float64(23), int64(21), object(62)
memory usage: 52.5+ MB

Carga de mapas

In [3]:
with open('src/geo/' + city + '.neighbourhoods.geojson') as f:
    city_nb = fix_geojson(json.load(f))

Descarte inicial de características

En el descarte inicial se desechan las características que se consideran menos útiles para el análisis exploratorio. Tras el análisis exploratorio habrá otro descarte de características que si bien habían sido interesantes para el exploratorio no lo serán para el modelado de la solución.

In [4]:
useless_cols = [
    'id',
    'listing_url',
    'scrape_id',
    'last_scraped',
    'name',
    'summary',
    'space',
    'description',
    'experiences_offered',
    'neighborhood_overview',
    'notes',
    'transit',
    'access',
    'interaction',
    'house_rules',
    'thumbnail_url',
    'medium_url',
    'picture_url',
    'xl_picture_url',
    'host_id',
    'host_url',
    'host_name',
    'host_since',
    'host_location',
    'host_about',
    'host_response_rate',
    'host_acceptance_rate',
    'host_is_superhost',
    'host_thumbnail_url',
    'host_picture_url',
    'host_neighbourhood',
    'host_listings_count',
    'host_total_listings_count',
    'host_has_profile_pic',
    'host_identity_verified',
    'host_verifications',
    'street',
    'neighbourhood',
    'city',
    'state',
    'zipcode',
    'market',
    'smart_location',
    'country_code',
    'country',
    'is_location_exact',
    'square_feet',
    'weekly_price',
    'monthly_price',
    'beds',
    'bed_type',
    'minimum_nights',
    'maximum_nights',
    'minimum_minimum_nights',
    'maximum_minimum_nights',
    'minimum_maximum_nights',
    'maximum_maximum_nights',
    'calendar_updated',
    'has_availability',
    'availability_30',
    'availability_60',
    'availability_90',
    'availability_365',
    'calendar_last_scraped',
    'requires_license',
    'jurisdiction_names',
    'is_business_travel_ready',
    'require_guest_profile_picture',
    'require_guest_phone_verification',
    'calculated_host_listings_count',
    'calculated_host_listings_count_entire_homes',
    'calculated_host_listings_count_private_rooms',
    'calculated_host_listings_count_shared_rooms'
]

df.drop(useless_cols, axis=1, inplace=True)

Armonización de barrios y distritos

Se unifican los nombres de los distritos de los distintos datasets para permitir join entre varios datasets cuando sea necesario. Para simplificar, se eliminan caracteres especiales como los acentos y se cambia el nombre de la columnas de barrio y distrito.

In [5]:
df.drop(['neighbourhood_group_cleansed'], axis=1, inplace=True)

df['neighbourhood'] = df['neighbourhood_cleansed']
df['neighbourhood'] = df['neighbourhood'].apply(lambda x: remove_accents(x))
df.drop(['neighbourhood_cleansed'], axis=1, inplace=True)

Características de tipo fecha

Se realiza la conversión de formato texto a formato fecha para facilitar posteriores operaciones.

In [6]:
df['first_review'] = pd.to_datetime(df['first_review'], format='%Y-%m-%d')
df['last_review'] = pd.to_datetime(df['last_review'], format='%Y-%m-%d')

Características dinerarias

Se realiza la conversión de formato texto en dólares a formato numérico en euros.

In [7]:
dollar_to_euro_rate = 0.9

dollar_cols = [
    'price',
    'security_deposit', 
    'cleaning_fee', 
    'extra_people'
]

for col in dollar_cols:
    if col in df.columns:
        df[col].fillna('$0', inplace=True)
        df[col] = df[col].apply(lambda x: clean_price_dollar(x))
        df[col] = df[col].astype(float)
        df[col] = df[col] * dollar_to_euro_rate
        df[col] = df[col].round(2)

Características binarias

Se realiza la conversión de formato texto (t para true, f para false) a formato numérico (0 para false, 1 para true).

In [8]:
bin_cols = [
    'host_is_superhost',
    'host_identity_verified', 
    'is_location_exact',
    'has_availability',
    'instant_bookable'
]
    
for col in bin_cols:
    if col in df.columns:
        df[col] = df[col].apply(lambda x: get_bin_value_by_char(x))
        df[col] = df[col].astype(int)

Adaptación de equipamientos

Se realiza la conversión de un formato no estructurado extrayendo del texto los tipos de equipamientos más relevantes que declara cada vivienda y creando una característica binaria para cada tipo de equipamiento. Esto además simplificará la gestión de variables categóricas en el modelado y facilitará en algunos aspectos la exploración.

In [9]:
df['amenities'] = df['amenities'].str.lower()
amenities_dict = {}

def collect_amenities(str):
    str = str.replace('{', '').replace('}', '').replace('\"', '').strip()
    word_list = str.split(",")
    for w in word_list:
        w = w.strip()
        amenities_dict[w] = amenities_dict.get(w, 0) + 1
    
df['amenities'].apply(lambda x: collect_amenities(x))
amenities_dict = sorted(amenities_dict.items(), key=lambda x: x[1], reverse=True)

top_amenities = [
    'wifi', 
    'essentials', 
    'kitchen', 
    'heating', 
    'washer', 
    'hangers', 
    'tv', 
    'hair dryer', 
    'iron', 
    'shampoo',
    'laptop friendly workspace',
    'air conditioning', 
    'hot water',
    'elevator',
    'refrigerator',
    'dishes and silverware',
    'microwave',
    'bed linens',
    'no stairs or steps to enter',
    'coffee maker',
    'cooking basics',
    'family/kid friendly',
    'long term stays allowed',
    'first aid kit',
    'oven',
    'stove'
]

for v in top_amenities:
    new_col_name = 'has_' + v.replace(' ', '_')
    df[new_col_name] = df['amenities'].apply(lambda x: contains_bin_value(v, x))
    df[new_col_name] = df[new_col_name].astype(int)
    
df.drop(['amenities'], axis=1, inplace=True)

Adaptación de las verificaciones del anfitrión

Se realiza la conversión de un formato no estructurado extrayendo del texto los tipos de verificaciones más relevantes que declara cada anfitrión y creando una característica binaria para cada tipo de verificación. Esto además simplificará la gestión de variables categóricas en el modelado y facilitará en algunos aspectos la exploración.

In [10]:
"""
df['host_verifications'] = df['host_verifications'].str.lower()
hverif_dict = {}

def collect_host_verifications(str):
    str = str.replace('[', '').replace(']', '').replace('\"', '').replace('\'', '').strip()
    word_list = str.split(",")
    for w in word_list: 
        w = w.strip()
        hverif_dict[w] = hverif_dict.get(w, 0) + 1
    
df['host_verifications'].apply(lambda x: collect_host_verifications(x))
hverif_dict = sorted(hverif_dict.items(), key=lambda x: x[1], reverse=True)

top_verification_modes = [
    'phone',
    'email',
    'government_id',
    'reviews',
    'jumio',
    'offline_government_id',
    'selfie',
    'identity_manual',
    'facebook',
    'work_email',
    'google'
]

for v in top_verification_modes:
    new_col_name = 'host_verified_by_' + v.replace(' ', '_')
    df[new_col_name] = df['host_verifications'].apply(lambda x: contains_bin_value(v, x))
    df[new_col_name] = df[new_col_name].astype(int)
    
df.drop(['host_verifications'], axis=1, inplace=True)
"""
Out[10]:
'\ndf[\'host_verifications\'] = df[\'host_verifications\'].str.lower()\nhverif_dict = {}\n\ndef collect_host_verifications(str):\n    str = str.replace(\'[\', \'\').replace(\']\', \'\').replace(\'"\', \'\').replace(\'\'\', \'\').strip()\n    word_list = str.split(",")\n    for w in word_list: \n        w = w.strip()\n        hverif_dict[w] = hverif_dict.get(w, 0) + 1\n    \ndf[\'host_verifications\'].apply(lambda x: collect_host_verifications(x))\nhverif_dict = sorted(hverif_dict.items(), key=lambda x: x[1], reverse=True)\n\ntop_verification_modes = [\n    \'phone\',\n    \'email\',\n    \'government_id\',\n    \'reviews\',\n    \'jumio\',\n    \'offline_government_id\',\n    \'selfie\',\n    \'identity_manual\',\n    \'facebook\',\n    \'work_email\',\n    \'google\'\n]\n\nfor v in top_verification_modes:\n    new_col_name = \'host_verified_by_\' + v.replace(\' \', \'_\')\n    df[new_col_name] = df[\'host_verifications\'].apply(lambda x: contains_bin_value(v, x))\n    df[new_col_name] = df[new_col_name].astype(int)\n    \ndf.drop([\'host_verifications\'], axis=1, inplace=True)\n'

Adaptación de la licencia

Lo relevante para el estudio es si tiene licencia o no, sin importar el identificador.

In [11]:
df['license'].fillna(0, inplace=True)
df['has_license'] = df['license'].apply(lambda x: 1 if x != 0 else 0)
df.drop(['license'], axis=1, inplace=True)

Nuevas características calculadas

Se van a generar una serie de características nuevas para enriquecer el estudio descriptivo.

Tiempo de actividad

Se trata del período que la vivienda lleva explotándose de forma efectiva y se estima como el tiempo transcurrido en meses desde la primera estancia hasta la última.

In [12]:
df['activity_months'] = (df['last_review'] - df['first_review']) / np.timedelta64(1, 'M')
df['activity_months'].fillna(0, inplace=True)

Ingresos por estancia y nivel de ocupación

El precio por noche (price) no es un indicador definitivo de los ingresos por estancia porque el precio de una estancia viene determinada por otros factores adicionales:

  • Nº de huéspedes (algunos están incluidos en la tarifa price (guests-included) y el resto pagan una tarifa extra por noche (extra-people)
  • Tasas de limpieza (cleaning-fee)
  • Nº de noches
  • Nº de huéspedes (accomodates) como capacidad máxima porque beds no está informado en muchos de los casos

Por ello también se puede calcular una variable que estime el coste total de una estancia media tomando como media de número de noches 5.2 y como número de huéspedes el valor medio entre el mínimo y el máximo de ocupantes de la vivienda.

In [13]:
df['cleaning_fee'].fillna(0, inplace=True)
df['extra_people'].fillna(0, inplace=True)
df['guests_included'].fillna(0, inplace=True)
df['accommodates'].fillna(0, inplace=True)

avg_days = 5.2

df['income_med_occupation'] = df.apply(
    lambda r: calculate_income_med_occupation(
        r['price'], 
        r['cleaning_fee'], 
        r['accommodates'], 
        r['extra_people'], 
        r['guests_included'], 
        avg_days), 
    axis=1
)

df['price_med_occupation_per_accommodate'] = df.apply(
    lambda r: calculate_price_med_occupation_per_accommodate(
        r['price'], 
        r['cleaning_fee'], 
        r['accommodates'], 
        r['extra_people'], 
        r['guests_included'], 
        avg_days), 
    axis=1
)

Valores extremos (outliers)

Tipo de propiedad

Se excluyen propiedades como los hoteles que ya de por sí son negocios hosteleros ya que el estudio pretende ceñirse a viviendas.

In [14]:
outliers_idx = df[~df['property_type'].isin(['Apartment', 'House', 'Chalet', 'Condominium', 'Loft'])].index
remove_outliers(df, outliers_idx, debug_col='property_type')
2801 outliers to be removed with values: ['Aparthotel', 'Barn', 'Bed and breakfast', 'Boat', 'Boutique hotel', 'Bungalow', 'Cabin', 'Camper/RV', 'Casa particular (Cuba)', 'Cave', 'Cottage', 'Dome house', 'Dorm', 'Earth house', 'Guest suite', 'Guesthouse', 'Hostel', 'Hotel', 'Houseboat', 'Igloo', 'Nature lodge', 'Other', 'Serviced apartment', 'Tiny house', 'Townhouse', 'Villa']
In [15]:
outliers_idx = df[df['room_type'].isin(['Hotel room'])].index
remove_outliers(df, outliers_idx, debug_col='room_type')
0 outliers to be removed with values: []

Viviendas sin reviews

Las viviendas sin reviews, ya que no hay ningún indicio para saber si llevan o no tiempo publicadas, se excluyen del estudio.

In [16]:
df['number_of_reviews'].fillna(0, inplace=True)
outliers_idx = df[df['number_of_reviews'] < 2]['number_of_reviews'].index.tolist()
remove_outliers(df, outliers_idx, debug_col='number_of_reviews')
18040 outliers to be removed with values: [0, 1]

Precios por huésped extremos

Se aplica arbitrariamente una regla de rango intercuartil (IQR) para excluir precios extremos.

In [17]:
outliers_idx = get_outliers_iqr(df['price_med_occupation_per_accommodate'], 4.5)[0]
remove_outliers(df, outliers_idx, debug_col='price_med_occupation_per_accommodate')
outliers between following bounds: -602.7749999999999 1210.725
224 outliers to be removed with values: [1212.12, 1214.46, 1215.0, 1219.32, 1222.5, 1224.0, 1229.4, 1229.76, 1230.0, 1233.0, 1238.4, 1238.76, 1239.3, 1241.82, 1242.0, 1248.0, 1251.0, 1251.9, 1252.8, 1261.8, 1263.6, 1269.0, 1270.26, 1270.8, 1272.96, 1273.5, 1281.6, 1283.85, 1287.0, 1288.39, 1290.6, 1293.0, 1294.65, 1305.0, 1305.72, 1306.8, 1308.0, 1308.6, 1309.5, 1314.0, 1315.8, 1317.6, 1318.5, 1323.0, 1324.8, 1326.6, 1332.0, 1336.14, 1337.4, 1340.46, 1343.79, 1346.4, 1357.2, 1364.4, 1373.4, 1374.54, 1383.3, 1386.0, 1397.52, 1404.0, 1405.8, 1411.2, 1413.36, 1413.54, 1417.5, 1422.9, 1425.6, 1430.1, 1434.0, 1438.2, 1440.0, 1449.0, 1449.22, 1453.86, 1458.0, 1467.0, 1470.42, 1471.5, 1481.4, 1482.0, 1485.0, 1488.0, 1496.25, 1496.52, 1497.6, 1516.32, 1518.75, 1519.92, 1521.0, 1521.54, 1522.08, 1539.0, 1540.35, 1542.6, 1543.5, 1548.0, 1551.06, 1551.6, 1555.62, 1567.5, 1569.6, 1599.75, 1604.4, 1609.2, 1611.9, 1617.3, 1652.4, 1660.5, 1665.0, 1669.5, 1692.0, 1711.8, 1713.6, 1728.0, 1755.0, 1756.8, 1767.42, 1777.5, 1788.75, 1793.25, 1798.02, 1876.5, 1883.7, 1894.5, 1905.75, 1916.82, 1929.0, 1939.5, 1972.5, 2082.6, 2106.0, 2119.5, 2137.5, 2142.0, 2174.4, 2191.5, 2204.28, 2214.0, 2224.53, 2233.53, 2242.08, 2243.88, 2249.1, 2250.0, 2285.28, 2340.0, 2358.0, 2367.0, 2370.0, 2385.0, 2388.06, 2417.22, 2428.92, 2445.0, 2619.0, 2638.8, 2682.0, 2740.5, 2769.3, 2790.0, 2835.0, 2853.0, 2878.2, 2946.6, 3083.04, 3285.0, 3321.0, 3439.8, 3600.0, 4212.0, 4338.0, 4446.0, 4460.76, 4462.56, 4472.46, 4675.32, 4680.0, 4689.0, 4691.25, 4702.5, 4716.0, 4947.75, 6247.5, 7800.0, 9396.0, 11005.83, 20425.14, 21358.8, 23400.0, 39780.0, 40135.68]

Número mínimo de noches extremo

Hay viviendas anunciadas cuyo mínimo de noches por estancia es tan elevado (meses, un año o varios años) que se puede deducir que su objetivo o su intención es un alquier de tipo residencial estable, no vacacional.

Se excluyen de forma arbitraria los pisos cuyo número mínimo de noches supere las 70 por considerarlo un alquiler no vacacional.

In [18]:
outliers_idx = df[df['minimum_nights_avg_ntm'] > 70].index
remove_outliers(df, outliers_idx, debug_col='minimum_nights_avg_ntm')
319 outliers to be removed with values: [72.0, 73.5, 75.0, 80.0, 85.0, 87.1, 90.0, 91.0, 92.0, 93.0, 93.9, 95.0, 99.0, 100.0, 112.8, 114.2, 118.0, 120.0, 124.5, 140.0, 141.3, 150.0, 160.0, 180.0, 182.0, 185.0, 200.0, 215.6, 222.0, 244.0, 250.0, 270.0, 275.0, 290.0, 300.0, 308.0, 332.0, 349.3, 360.0, 365.0, 366.0, 500.0, 600.0, 999.0, 1000.0, 1001.0, 1124.0, 9999.0]

Análisis exploratorio

Las características que a priori son las más importantes en el problema que se plantea son los precios y las reviews, por la relación directa en los ingresos del anfitrión y en los costes para el huésped.

Las reviews constituyen la manera más cercana de estimar el nivel de ocupación de la vivienda a falta de datos explícitos al respecto como podrían ser las duraciones de las estancias o el número de huéspedes que definen cada estancia.

En los siguientes apartados se analizan diferentes relaciones entre las múltiples características de los alojamientos, siendo especialmente relevantes los precios y las reviews como se comentaba.

Distribución del precio

El precio price es la variable objetivo del estudio luego es conveniente ver de partida cómo está distribuido.

Entre 40 y 120 euros por noche se concentra la gran mayoría de alojamientos.

In [19]:
fig = px.histogram(df, x="price", nbins=40)
fig.show()
In [20]:
fig = ff.create_distplot([df['price']], ['price'], bin_size=[25])
fig.show()

Resumen del resto de características

In [21]:
pandas_profiling.ProfileReport(df)
Out[21]:

Correlaciones

En los mapas de correlación se pueden obtener algunos indicios interesantes de relaciones entre características a parte de las relaciones evidentes por familias como por ejemplo la familia de los precios o la familia de las reviews.

In [22]:
def print_corr_map(df, height=None):
    corrs = df.corr()
    fig = go.Figure(
        data=go.Heatmap(
            z=corrs.values,
            x=list(corrs.columns),
            y=list(corrs.index),
            showscale=True
        )
    )
    
    if height:
        fig.update_layout(height=height)
        
    fig.show()
In [23]:
key_cols = [
    'price',
    'price_med_occupation_per_accommodate',
    'income_med_occupation',
    'review_scores_rating',
    'reviews_per_month'
]
In [24]:
misc_cols = [
    'activity_months',
    'accommodates',
    'bathrooms', 
    'bedrooms',
    'cancellation_policy', 
    'cleaning_fee',
    'extra_people',
    'first_review',
    'guests_included',
    'instant_bookable',
    'has_license',
    'host_response_time',
    'latitude',
    'longitude',
    'maximum_nights_avg_ntm',
    'minimum_nights_avg_ntm',
    'neighbourhood',
    'number_of_reviews',
    'number_of_reviews_ltm',
    'property_type',
    'room_type',
    'security_deposit',
    *key_cols
]

print_corr_map(df[misc_cols], height=900)
In [25]:
review_cols = [
    'activity_months',
    'instant_bookable',
    'review_scores_accuracy',
    'review_scores_cleanliness',
    'review_scores_checkin',
    'review_scores_communication',
    'review_scores_location',
    'review_scores_value',
    *key_cols
]

print_corr_map(df[review_cols])

Licencia

La mayoría de las viviendas no presentan la licencia turística aunque no parece ser un asunto que penalice demasiado a la hora de ser alquilado en estos momentos.

In [26]:
df_by_license = df.groupby(['has_license'])['has_license'].count().to_frame('has_license_count')
df_by_license.reset_index(inplace=True)

fig32 = go.Figure(
        go.Pie(
            labels=['no_license', 'has_license'], 
            values=df_by_license['has_license_count']
        )
)

fig32.update_traces(
    textfont_size=20,
    marker=dict(colors=['Orange', 'SteelBlue'])
)

fig32.update_layout(title='license')
fig32.show()

fig322 = go.Figure()
for val in [0, 1]:
    fig322.add_trace(
        go.Violin(
            x=df['has_license'][df['has_license'] == val],
            y=df['reviews_per_month'][df['has_license'] == val],
            name=val,                
            meanline_visible=True,
            line_color='Orange' if val == 0 else 'SteelBlue'
        )
    )

fig322.update_layout(title='reviews_per_month x has_license', showlegend=False)
fig322.show()

Tipos de propiedad y relación con precio por huésped

Claro dominio de apartamentos. En el precio no hay diferencias notables.

In [27]:
df_by_property_type = df.groupby(['property_type'])['property_type'].count().to_frame('property_type_count')
df_by_property_type.reset_index(inplace=True)
property_types = np.sort(df_by_property_type['property_type'].unique())

fig33 = go.Figure(go.Pie(
    labels=df_by_property_type['property_type'], 
    values=df_by_property_type['property_type_count']
))

fig33.update_layout(title='property_type')
fig33.update_traces(textfont_size=15)
fig33.show()

fig332 = go.Figure()
for pt in property_types:
    fig332.add_trace(
        go.Violin(
            x=df['property_type'][df['property_type'] == pt],
            y=df['price_med_occupation_per_accommodate'][df['property_type'] == pt],
            points='all',
            name=pt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig332.update_layout(title='price_med_occupation_per_accommodate')
fig332.show()

Tipos de habitaciones y relación con precio por huésped

El formato más habitual de alquiler vacacional es el piso completo, por encima de la habitación privada en piso compartido. Los precios van en sintonía con ello.

In [28]:
df_by_room_type = df.groupby(['room_type'])['room_type'].count().to_frame('room_type_count')
df_by_room_type.reset_index(inplace=True)
room_types = np.sort(df_by_room_type['room_type'].unique())

fig34 = go.Figure(
        go.Pie(
            labels=df_by_room_type['room_type'], 
            values=df_by_room_type['room_type_count']
        )
)

fig34.update_traces(textfont_size=20)
fig34.update_layout(title='room_type')
fig34.show()

fig342 = go.Figure()
for rt in room_types:
    fig342.add_trace(
        go.Violin(
            x=df['room_type'][df['room_type'] == rt],
            y=df['price_med_occupation_per_accommodate'][df['room_type'] == rt],
            points='all',
            name=rt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig342.update_layout(title='price_med_occupation_per_accommodate')
fig342.show()

Relación entre número y puntuación de las reviews

Puede haber una cierta retroalimentación entre el número de reviews por mes y la nota en las mismas. Lo normal es que cuantas más y mejores reviews tenga una vivienda, más estancias genere.

In [29]:
px.scatter(
    df, 
    x='review_scores_rating', 
    y='reviews_per_month', 
    color='room_type'
).show()

Relación entre política de cancelación y número de reviews mensuales

No parece que haya un claro impacto del tipo de cancelación sobre el número de estancias. En general los anfitriones utilizan políticas de cancelación que tienen un mínimo de flexibilidad en el tiempo para no ahuyentar a potenciales huéspedes.

In [30]:
df_by_cancellation_policy = df.groupby(['cancellation_policy'])['cancellation_policy'].count().to_frame('cancellation_policy_count')
df_by_cancellation_policy.reset_index(inplace=True)
cancellation_policy_types = np.sort(df_by_cancellation_policy['cancellation_policy'].unique())

fig36 = go.Figure(go.Pie(
    labels=df_by_cancellation_policy['cancellation_policy'], 
    values=df_by_cancellation_policy['cancellation_policy_count']
))

fig36.update_layout(title='cancellation_policy')
fig36.update_traces(textfont_size=15)
fig36.show()

fig362 = go.Figure()
for cpt in cancellation_policy_types:
    fig362.add_trace(
        go.Violin(
            x=df['cancellation_policy'][df['cancellation_policy'] == cpt],
            y=df['reviews_per_month'][df['cancellation_policy'] == cpt],
            name=cpt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig362.update_layout(title='reviews_per_month', showlegend=False)
fig362.show()

Relación entre número mínimo de noches y número de reviews mensuales

El número mínimo de noches a la hora de reservar un alojamiento podría ser un factor limitante. Simplemente es un indicio porque no se conoce la duración de las estancias pero los alojamientos que menos limitan este aspecto tienen más estancias al mes.

In [31]:
px.scatter(
    df, 
    x='minimum_nights_avg_ntm', 
    y='reviews_per_month'
).show()

Relación entre tiempo de respuesta y número de reviews mensuales

A mejor tasa de respuesta, más estancias. Es importante por tanto la agilidad de los anfitriones a la hora de tratar con los potenciales huéspedes.

In [32]:
df['host_response_time'].fillna('-unk-', inplace=True)
df_by_response_time = df.groupby(['host_response_time'])['host_response_time'].count().to_frame('host_response_time_count')
df_by_response_time.reset_index(inplace=True)
response_time_types = np.sort(df_by_response_time['host_response_time'].unique())

fig38 = go.Figure()
for rtt in response_time_types:
    fig38.add_trace(
        go.Violin(
            x=df['host_response_time'][df['host_response_time'] == rtt],
            y=df['reviews_per_month'][df['host_response_time'] == rtt],
            name=rtt,
            box_visible=True,                
            meanline_visible=True
        )
    )

fig38.update_layout(title='reviews_per_month', showlegend=False)
fig38.show()

Tasa de limpieza

Una mayor tasa de limpieza se relaciona con viviendas que se alquilan de forma completa. Cuanto más alta es la tasa de limpieza, menos reservas al mes se obtienen posiblemente por tratarse de viviendas exclusivas.

In [33]:
px.scatter(
    df, 
    x='cleaning_fee', 
    y='reviews_per_month', 
    color='room_type'
).show()
In [34]:
px.scatter(
    df, x='cleaning_fee', 
    y='income_med_occupation', 
    color='room_type'
).show()

Número de alojamientos por barrio

La concentración de alojamientos varía según el barrio. No hay una tendencia definida.

In [35]:
df_by_nb = df.groupby(['neighbourhood'])['neighbourhood'].size().to_frame('count')
df_by_nb.reset_index(inplace=True)

fig310 = go.Figure(go.Choroplethmapbox(
    geojson=city_nb,
    locations=df_by_nb['neighbourhood'], 
    z=df_by_nb['count'],                   
    colorscale='Blues',                                
    marker_opacity=0.5, 
    marker_line_width=0.5
))

fig310.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig310.show()

Relación entre precio por húesped y localización

En general son los barrios del centro los que presentan precios más altos.

In [36]:
fig311 = go.Figure(
    go.Scattermapbox(
        lon=df['longitude'],
        lat=df['latitude'],
        mode='markers',
        marker_color=df['price_med_occupation_per_accommodate'],
        text=df['price_med_occupation_per_accommodate'],
        marker=dict(
            opacity=0.5, 
            colorscale='Blues',
            cmin=df['price'].min(), 
            cmax=df['price'].max()
        )
    )
)

fig311.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig311.show()

Relación entre precio por huésped y notas de review

Las notas de las reviews no tienen una relación clara con el precio pagado por el huésped (ya sea alto o bajo).

In [37]:
px.scatter(
    df, 
    x='review_scores_rating',
    y='price_med_occupation_per_accommodate'
).show()

Relación entre precio por huésped y número de reviews mensuales

Los alojamientos con precios por noche en el rango entre 150 y 290 euros son los que consiguen más estancias.

In [38]:
px.scatter(
    df,
    x='price_med_occupation_per_accommodate',
    y='reviews_per_month',
    marginal_x='box', 
    trendline='ols'
).show()

Review de localización

La valoración de localización es un aspecto muy abierto e interesante. Se puede considerar una localización en función de si está bien conectada con el resto de la ciudad, o de si está próxima a puntos de interés, o de si está bien ubicado con respecto a cualquier intención concreta ya sea de tipo turístico, laboral, familiar, etc. que cualquier huésped puede tener.

En el mapa se muestra que efectivamente los barrios del centro tienen una gran calificación en este aspecto pero tampoco hay diferencias muy grandes con barrios un poco más lejanos al centro.

In [39]:
df_by_nb = df.groupby(['neighbourhood'])['review_scores_location'].mean().to_frame('review_scores_location_avg')
df_by_nb.reset_index(inplace=True)

fig3142 = go.Figure(go.Choroplethmapbox(
    geojson=city_nb,
    locations=df_by_nb['neighbourhood'], 
    z=df_by_nb['review_scores_location_avg'],                   
    colorscale='Blues',                                
    marker_opacity=0.5, 
    marker_line_width=0.5
))

fig3142.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig3142.show()

Distribución del precio por huésped

Entre 75 y 350 euros por noche es el rango más habitual de precio que paga un huésped por noche por una estancia de duración media.

In [40]:
fig = px.histogram(df, x='price_med_occupation_per_accommodate', nbins=40)
fig.show()
In [41]:
fig = go.Figure()

fig.add_trace(go.Histogram(
    x=df['price_med_occupation_per_accommodate'],
    name='price_med_occupation_per_accommodate',
    marker_color='#EB89B5'))

fig.add_trace(go.Histogram(
    x=df['price'],
    name='price',
    marker_color='#330C73'))

"""
fig.add_trace(go.Histogram(
    x=df['income_med_occupation'],
    name='income_med_occupation',
    marker_color='green'))
"""

fig.update_layout(barmode='stack')
fig.update_traces(opacity=0.75)
fig.show()

Relación entre habitaciones y baños

Dominan con claridad los alojamientos con pocas estancias, en concreto con un baño y un dormitorio.

In [42]:
df['bedrooms'].fillna(0, inplace=True)
plt.figure(figsize=(16, 8))
plt.hist(df['bedrooms'], bins=15)
plt.gca().set(title='BEDROOMS', ylabel='COUNT');
In [43]:
df['bathrooms'].fillna(0, inplace=True)
plt.figure(figsize=(16, 8))
plt.hist(df['bathrooms'], bins=15)
plt.gca().set(title='BATHROOMS', ylabel='COUNT');
In [44]:
df_bb = df.groupby(['bedrooms', 'bathrooms'])[['bedrooms', 'bathrooms']].size().to_frame('cnt')
df_bb.reset_index(inplace=True)
fig316 = px.scatter(df_bb, x='bedrooms', y='bathrooms', size='cnt')
fig316.show()

Relación entre ingresos por estancia y capacidad

In [45]:
df_by_price_per_bed = df.groupby(['neighbourhood', 'accommodates'])['income_med_occupation'].mean().to_frame('income_med_occupation_avg')
df_by_price_per_bed.reset_index(inplace=True)

px.line(
    df_by_price_per_bed,
    x='accommodates', 
    y='income_med_occupation_avg',
    color='neighbourhood'
).show()

Distribución de ingresos por estancia según el barrio

In [46]:
nbs = np.sort(df['neighbourhood'].unique())[::-1]      
    
fig318 = go.Figure()
for d in nbs:
    fig318.add_trace(go.Violin(
        x=df[df['neighbourhood'] == d]['income_med_occupation'].values,
        name='  ' + d + '  '
    ))

fig318.update_traces(
    orientation='h', 
    side='positive', 
    width=3, 
    points=False
)

fig318.update_layout(
    height=900,
    xaxis_showgrid=False, 
    xaxis_zeroline=False, 
    showlegend=False
)

fig318.show()
In [47]:
px.scatter(
    df,
    x='accommodates',
    y='income_med_occupation', 
    trendline='ols',
    color='neighbourhood'
).show()

Relación entre barrio y precio por huésped

En general son los barrios más céntricos los que mayor precio por huésped tienen aunque hay diversas excepciones.

Los barrios periféricos en general presentan precios más bajos que el resto.

In [48]:
df_by_nb = df.groupby(['neighbourhood'])['price_med_occupation_per_accommodate'].mean().to_frame('price_med_occupation_per_accommodate_avg')
df_by_nb.reset_index(inplace=True)

fig320 = go.Figure(go.Choroplethmapbox(
    geojson=city_nb,
    locations=df_by_nb['neighbourhood'], 
    z=df_by_nb['price_med_occupation_per_accommodate_avg'],                   
    colorscale='Blues',                                
    marker_opacity=0.5, 
    marker_line_width=0.5
))

fig320.update_layout(
    mapbox_style='carto-positron',
    mapbox_zoom=11, 
    mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
    margin={"r":0,"t":0,"l":0,"b":0}
)

fig320.show()

Relación entre precio por huésped e ingresos por estancia media

A lo largo del estudio se han presentado diferentes relaciones entre características donde participan el precio medio por noche y huésped y los ingresos por estancia media. Ambas variables están basadas en la característica precio y por ello tienen una clara relación con ella así que es probable que en la fase de modelado se descarte el uso de alguna de ellas.

In [49]:
fig = go.Figure()

fig.add_trace(go.Histogram(
    x=df['price_med_occupation_per_accommodate'],
    name='price_med_occupation_per_accommodate',
    marker_color='#EB89B5'))

fig.add_trace(go.Histogram(
    x=df['income_med_occupation'],
    name='income_med_occupation',
    marker_color='blue'))

fig.update_layout(barmode='stack')
fig.update_traces(opacity=0.75)
fig.show()
In [50]:
fig323 = go.Figure()
for x in ['price_med_occupation_per_accommodate', 'income_med_occupation']:
    fig323.add_trace(go.Scatter(
        x=df['price'], 
        y=df[x], 
        mode='markers',
        name=x,
        marker=dict(color=('blue' if x != 'price_med_occupation_per_accommodate' else '#EB89B5'))
    ))
    fig323.update_traces(marker=dict(opacity=0.5))

fig323.show()

Equipamiento de la vivienda

En las correlaciones simplemente se observa más afinidad entre objetos que normalmente se ubican en una cocina como el frigorífico, el microondas, la cafetera o la vajilla.

No existe un impacto claro entre alguna comodidad concreta y las reviews en número o nota de las estancias.

Curiosamente el aire acondicionado es una de las comodidades que menos destaca, bien es cierto que es algo que tiene importancia sobre todo de forma estacional y según para qué tipo de huéspedes.

In [52]:
reviews_cols = [
    'reviews_per_month', 
    'review_scores_rating'
]

amenities_cols = [
    'has_air_conditioning',
    'has_bed_linens',
    'has_coffee_maker',
    'has_cooking_basics',
    'has_dishes_and_silverware',
    'has_elevator',
    'has_essentials', 
    'has_family/kid_friendly',
    'has_first_aid_kit',
    'has_hair_dryer',
    'has_hangers',
    'has_heating',
    'has_hot_water',
    'has_iron',
    'has_kitchen',
    'has_laptop_friendly_workspace',
    'has_microwave',
    'has_no_stairs_or_steps_to_enter',
    'has_oven',
    'has_refrigerator',
    'has_shampoo',
    'has_stove',
    'has_tv',
    'has_washer',
    'has_wifi'
]

print_corr_map(df[[*amenities_cols, *reviews_cols]], height=900)

"""
for am in amenities_cols:
    fig324 = go.Figure()
    fig324 = make_subplots(
        rows=1, 
        cols=2, 
        subplot_titles=('reviews_per_month', 'review_scores_rating')
    )
    
    for var in reviews_cols:
        for val in [0, 1]:
            fig324.add_trace(go.Violin(
                    x=df[am][df[am] == val],
                    y=df[var][df[am] == val],
                    name=val,                
                    meanline_visible=True,
                    line_color='Orange' if val == 0 else 'SteelBlue'
                ),
                row=1, 
                col=1 if var == 'reviews_per_month' else 2
            )

        fig324.update_layout(title=am, showlegend=False)
        
    fig324.show()
"""
Out[52]:
"\nfor am in amenities_cols:\n    fig324 = go.Figure()\n    fig324 = make_subplots(\n        rows=1, \n        cols=2, \n        subplot_titles=('reviews_per_month', 'review_scores_rating')\n    )\n    \n    for var in reviews_cols:\n        for val in [0, 1]:\n            fig324.add_trace(go.Violin(\n                    x=df[am][df[am] == val],\n                    y=df[var][df[am] == val],\n                    name=val,                \n                    meanline_visible=True,\n                    line_color='Orange' if val == 0 else 'SteelBlue'\n                ),\n                row=1, \n                col=1 if var == 'reviews_per_month' else 2\n            )\n\n        fig324.update_layout(title=am, showlegend=False)\n        \n    fig324.show()\n"

Exportación dataset

In [53]:
df.to_csv(filename_out, index=False)